package com.hzgj.bcl.soa.cicada.handle;

import com.hzgj.bcl.soa.cicada.AsyncHandler;
import com.hzgj.bcl.soa.cicada.ListenableFuture;
import com.hzgj.bcl.soa.cicada.pool.Channels;
import com.hzgj.bcl.soa.cicada.timeout.TimeoutHolder;
import com.hzgj.bcl.soa.util.Util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.SocketAddress;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import io.netty.channel.Channel;
import lombok.Getter;
import lombok.Setter;

public final class ResponseFuture<V> implements ListenableFuture<V> {

    private static final Logger logger = LoggerFactory.getLogger(ResponseFuture.class);
    private final long start = Util.millisTime();
    private final CountDownLatch latch = new CountDownLatch(1);
    // state mutated from outside the event loop
    // TODO check if they are indeed mutated outside the event loop
    private final AtomicBoolean isDone = new AtomicBoolean(false);
    private final AtomicBoolean isCancelled = new AtomicBoolean(false);
    private final AtomicLong touch = new AtomicLong(Util.millisTime());
    private final AtomicReference<STATE> state = new AtomicReference<>(STATE.NEW);
    private final AtomicBoolean onThrowableCalled = new AtomicBoolean(false);
    private final AtomicReference<V> content = new AtomicReference<>();
    private final AtomicReference<ExecutionException> exEx = new AtomicReference<>();
    private volatile TimeoutHolder timeoutHolder;
    @Setter
    @Getter
    private boolean reuse;
    // state mutated only inside the event loop
    private Channel channel;
    @Getter
    private AsyncHandler<V> asyncHandler;
    @Getter
    @Setter
    private String poolKey;

    public ResponseFuture(AsyncHandler<V> asyncHandler) {
        this.asyncHandler = asyncHandler;
    }

    @Override
    public boolean isDone() {
        return isDone.get() || isCancelled();
    }

    @Override
    public boolean isCancelled() {
        return isCancelled.get();
    }

    @Override
    public boolean cancel(boolean force) {
        cancelTimeouts();

        if (isCancelled.getAndSet(true)) {
            return false;
        }

        // cancel could happen before channel was attached
        if (channel != null) {
            Channels.setDiscard(channel);
            Channels.silentlyCloseChannel(channel);
        }

        if (!onThrowableCalled.getAndSet(true)) {
            try {
                asyncHandler.onThrowable(new CancellationException());
            } catch (Throwable t) {
                logger.warn("cancel", t);
            }
        }
        latch.countDown();
        return true;
    }

    public void attachChannel(Channel channel) {

        if (isDone()) {
            Channels.setDiscard(channel);
            Channels.silentlyCloseChannel(channel);
        }

        this.channel = channel;
    }

    public void detachChannel() {
        this.channel = null;
    }

    @Override
    public V get() throws InterruptedException, ExecutionException {
        latch.await();
        return getContent();
    }

    @Override
    public V get(long l, TimeUnit tu) throws InterruptedException, TimeoutException, ExecutionException {
        if (!latch.await(l, tu)) {
            throw new TimeoutException();
        }
        return getContent();
    }

    private V getContent() throws ExecutionException {

        if (isCancelled()) {
            throw new CancellationException();
        }

        ExecutionException e = exEx.get();
        if (e != null) {
            throw e;
        }

        V update = content.get();
        return update;
    }

    public void updateContent(V update) {
        try {
            asyncHandler.onCompleted(update);
            content.compareAndSet(null, update);
        } catch (Throwable ex) {
            if (!onThrowableCalled.getAndSet(true)) {
                try {
                    try {
                        asyncHandler.onThrowable(ex);
                    } catch (Throwable t) {
                        logger.debug("asyncHandler.onThrowable", t);
                    }
                    throw new RuntimeException(ex);
                } finally {
                    cancelTimeouts();
                }
            }
        }
    }

    private boolean terminateAndExit() {
        cancelTimeouts();
        return isDone.getAndSet(true) || isCancelled.get();
    }

    @Override
    public final void done() {

        if (terminateAndExit()) {
            return;
        }

        try {
            getContent();
        } catch (ExecutionException t) {
            return;
        } catch (RuntimeException t) {
            Throwable exception = t.getCause() != null ? t.getCause() : t;
            exEx.compareAndSet(null, new ExecutionException(exception));

        } finally {
            latch.countDown();
        }
    }

    @Override
    public final void abort(final Throwable t) {

        exEx.compareAndSet(null, new ExecutionException(t));

        if (terminateAndExit()) {
            return;
        }

        if (onThrowableCalled.compareAndSet(false, true)) {
            try {
                asyncHandler.onThrowable(t);
            } catch (Throwable te) {
                logger.debug("asyncHandler.onThrowable", te);
            }
        }
        latch.countDown();
    }

    @Override
    public void touch() {
        touch.set(Util.millisTime());
    }

    public void cancelTimeouts() {
        if (timeoutHolder != null) {
            timeoutHolder.cancel();
            timeoutHolder = null;
        }
    }

    public void setTimeoutHolder(TimeoutHolder timeoutHolder) {
        this.timeoutHolder = timeoutHolder;
    }

    public STATE getState() {
        return state.get();
    }

    public void setState(STATE state) {
        this.state.set(state);
    }

    public long getLastTouch() {
        return touch.get();
    }

    public Channel channel() {
        return channel;
    }

    public SocketAddress getChannelRemoteAddress() {
        return channel != null ? channel.remoteAddress() : null;
    }

    public long getStart() {
        return start;
    }

    public enum STATE {
        NEW, POOLED, RECONNECTED, CLOSED, REINVOKE
    }

}
